﻿using System;
using System.IO;
using SharpDX;
using SharpDX.Direct3D9;
using System.Collections.Generic;

namespace FxArchitect
{
    class IncludeHandler : Include
    {
        string directory;
        Action<string> includeDelegate;

        public IncludeHandler(string directory, Action<string> includeDelegate = null)
        {
            this.directory = directory;
            this.includeDelegate = includeDelegate;
        }

        #region Include Members

        public void Close(Stream stream)
        {
            if (stream != null)
                stream.Close();
        }

        public Stream Open(IncludeType type, string fileName, Stream parentStream)
        {
            string file = !string.IsNullOrWhiteSpace(directory) ? 
                            Path.Combine(directory, fileName) :
                            fileName;

            if (includeDelegate != null)
                includeDelegate(file);

            return new FileStream(file, FileMode.Open);
        }

        #endregion

        #region ICallbackable Members

        public IDisposable Shadow { get { return this; } set { } }

        #endregion

        #region IDisposable Members

        public void Dispose() { }

        #endregion
    }

    public static class XEffectCompiler
    {
        const string fxProfile = "fx_2_0";
        static readonly Macro[] preprocessorMacros = new Macro[]
        {
            new Macro("WINDOWS", null)
        };

        /// <summary>
        /// Compile an effect to byte code.
        /// </summary>
        /// <param name="root">Root filename to display in error messages.</param>
        /// <param name="directory">Directory which will serve as the root for '#include' statements.</param>
        /// <param name="effectCode">String containing the effect source code.</param>
        /// <param name="debugBuild">Should the effect be compiled with debug flags?</param>
        public static byte[] CompileEffect(string root, string directory, string effectCode, 
                    out List<XEffectErrorOrWarning> errorsAndWarnings, bool debugBuild = true)
        {
            List<string> includeFiles = new List<string>();
            errorsAndWarnings = new List<XEffectErrorOrWarning>();

            ShaderBytecode result = null;
            ShaderFlags shaderFlags = ShaderFlags.None;
            IncludeHandler includeHandler = new IncludeHandler(directory, 
                                                               new Action<string>((file) =>
            {
                includeFiles.Add(file);
            }));
            
            if (!debugBuild)
            {
                try
                {
                    result = ShaderBytecode.Compile(effectCode, fxProfile, shaderFlags,
                                                    preprocessorMacros, includeHandler);
                }
                catch (CompilationException ex)
                {
                    ProcessErrorsAndWarnings(root, ex.Message, errorsAndWarnings);

                    // find if any of the reported problems is an error
                    bool isError = false;
                    for (int i = 0; 
                         i < errorsAndWarnings.Count && 
                         (isError = !errorsAndWarnings[i].IsWarning); 
                         i++) ;

                    // when there is an error in release builds, try recompiling as debug build
                    if (isError)
                    {
                        shaderFlags |= ShaderFlags.Debug | ShaderFlags.SkipOptimization;
                        debugBuild = true;
                        errorsAndWarnings.Clear();
                        result = null;
                    }
                }
            }

            if (debugBuild)
            {
                try
                {
                    result = ShaderBytecode.Compile(effectCode, fxProfile, shaderFlags,
                                                    preprocessorMacros, includeHandler);
                }
                catch (CompilationException ex)
                {
                    ProcessErrorsAndWarnings(root, ex.Message, errorsAndWarnings);

                    // find if any of the reported problems is an error
                    bool isError = false;
                    for (int i = 0;
                         i < errorsAndWarnings.Count &&
                         (isError = !errorsAndWarnings[i].IsWarning);
                         i++) ;

                    // when there is an error in debug builds the byte code is irrelevant
                    if (isError)
                        result = null;
                }
            }

            // if there is a compiled result at this point, send back the code from it
            if (result != null)
            {
                byte[] byteCode = new byte[result.BufferSize];
                result.Data.Read(byteCode, 0, byteCode.Length);
                return byteCode;
            }
            return null;
        }

        static void ProcessErrorsAndWarnings(string root, string errorsAndWarnings, 
                                             List<XEffectErrorOrWarning> errorOutput)
        {
            string[] errors = errorsAndWarnings.Split('\n');
            for (int i = 0; i < errors.Length; i++)
            {
                if (errors[i].StartsWith(Environment.NewLine))
                    break;

                // find some unique characters in the error string
                int openIndex = errors[i].IndexOf('(');
                int closeIndex = errors[i].IndexOf(')');

                // can't process the message if it has no line counter
                if (openIndex == -1 || closeIndex == -1)
                    continue;

                // find the error number, then move forward into the message
                int errorIndex = errors[i].IndexOf('X', closeIndex);
                if (errorIndex < 0)
                {
                    errorOutput.Add(new XEffectErrorOrWarning()
                    {
                        Description = errors[i],
                        Root = root,
                        Type = "Error"
                    });
                    continue;
                }

                // trim out the data we need to feed the logger
                string fileName = errors[i].Remove(openIndex);
                string lineAndColumn = errors[i].Substring(openIndex + 1, closeIndex - openIndex - 1);
                string description = errors[i].Substring(errorIndex);
                string[] lc = lineAndColumn.Split(',');

                //// when the file name is not present, the error can be found in the root file
                //if (string.IsNullOrEmpty(fileName))
                //    fileName = sourceFilename;

                // ensure that the file data points toward the correct file
                FileInfo fileInfo = new FileInfo(fileName);
                if (fileInfo.Name == "memory") fileName = root;
                //if (!fileInfo.Exists)
                //{
                //    FileInfo parentFile = new FileInfo(sourceFilename);
                //    fileInfo = new FileInfo(Path.Combine(parentFile.Directory.FullName, fileName));
                //}
                //fileName = fileInfo.FullName;

                // construct the temporary content identity and file the error or warning
                if (errors[i].Contains("warning"))
                {
                    errorOutput.Add(new XEffectErrorOrWarning()
                    {
                        Description = errors[i],
                        Line = lc.Length > 0 ? lc[0] : string.Empty,
                        Column = lc.Length > 1 ? lc[1] : string.Empty,
                        File = fileName,
                        Root = root,
                        IsWarning = true,
                        Type = "Warning"
                    });
                }
                else if (errors[i].Contains("error"))
                {
                    // handle the stupid non-conformant error messages
                    if (description.StartsWith("ID3DXEffectCompiler") ||
                        description.StartsWith("XEffectCompiler"))
                    {
                        errorIndex = errors[0].IndexOf('X');
                        int endOfErrorIndex = errors[0].IndexOf(';', errorIndex);
                        description = errors[0].Substring(errorIndex, endOfErrorIndex - errorIndex);
                    }

                    errorOutput.Add(new XEffectErrorOrWarning()
                    {
                        Description = description,
                        Line = lc.Length > 0 ? lc[0] : string.Empty,
                        Column = lc.Length > 1 ? lc[1] : string.Empty,
                        File = fileName,
                        Root = root,
                        IsWarning = false,
                        Type = "Error"
                    });
                }
            }
        }
    }
}
